<?php

namespace s94\wechat;

use Exception;

class Base
{
    protected $logList = [];
    protected $config = [];
    public function __construct($config)
    {
        $this->config = $config;
    }

    /**获取、设定配置
     * @param string|array $name 获取配置的配置名(string)、设定的配置数据(array)
     * @param mixed $default 默认值，不传的情况，如果配置项为空，会抛出异常
     * @return mixed|string|void
     * @throws Exception
     */
    public function config($name=null, $default=false)
    {
        if (is_array($name)){
            $this->config = array_merge($this->config, $name);
        }elseif ($name===null){
            return $this->config;
        }elseif(is_string($name)){
            if ($default===false){
                self::assert(!empty($this->config[$name]),"配置【{$name}】不能为空");
                return $this->config[$name];
            }else{
                return $this->config[$name] ?? $default;
            }
        }
    }

    /**随机字符串
     * @param int $length 字符串长度
     * @return string
     * @throws Exception
     */
    protected static function randomStr(int $length=16): string
    {
        $string = '';
        while (($len = strlen($string)) < $length) {
            $size = $length - $len;
            $bytes = random_bytes($size);
            $string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size);
        }
        return $string;
    }
    /**断言
     * @param mixed $pass 是否通过，判断为false的时候，会抛出异常，中断程序
     * @param string $message 异常消息
     * @return void
     * @throws Exception
     */
    protected static function assert($pass, string $message){
        if (!$pass) throw new Exception($message);
    }
    /**日志记录、获取
     * @param mixed $data 需要记录的数据，不传表示获取所有记录的日志数据
     * @return array|void
     */
    public function log(){
        if (func_num_args()>0){
            $this->logList[date('Y-m-d H:i:s')] = func_get_arg(0);
        }else{
            return $this->logList;
        }
    }

    /**multipart/form-data数据编码
     * @param array $param 数据内容，文件请使用CurlFileData
     * @param string $boundary 分界字符
     * @return string 编码后的字符数据
     */
    public static function multipart_form_data(array $param, string $boundary)
    {
        $data = '--' . $boundary;
        foreach ($param as $name => $value) {
            $data .= "\r\n";
            if ($value instanceof CurlFileData) {
                $filename = $value->getName();
                $mimetype = $value->getMime();
                $content = $value->getContent();
                $name = $name.'"; filename="'.$filename;
            } else {
                $mimetype = 'text/plain;charset=UTF-8';
                if (!is_string($value)) $value = json_encode($value);
                $content = $value;
            }
            $data .= "Content-Disposition: form-data; name=\"{$name}\"\r\n";
            $data .= "Content-Type: {$mimetype}\r\n\r\n";
            $data .= "{$content}\r\n";
            $data .= "--{$boundary}";
        }
        $data .= "--\r\n\r\n";

        return $data;
    }
    /**curl请求
     * @param string $url 请求地址
     * @param array $header 请求头消息，格式：['键: 值',...]。例如：['Content-type: text/plain', 'Content-length: 100']
     * @param array|string $post_data post参数，不为 null 表示post请求
     * @param array $option 需要自定义的 option 选项
     * @return array 返回响应数据，数据格式：['code'=>HTTP状态码,'header'=>头消息(array[键=>值]), 'body'=>报文主体(string)];
     * @throws Exception
     */
    public static function curl(string $url, array $header=[], $post_data=null, array $option=[]): array
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        $header_arr = [];
        $header_count = count($header);
        if (($header_count-1)*$header_count/2 != array_sum(array_keys($header))){
            foreach($header as $k=>$v){
                $header_arr[] = $k.": ".$v;
            }
        }else{
            $header_arr = $header;
        }
        $default_option = [
            CURLOPT_HEADER=> true, //将头文件的信息作为数据流输出
            CURLOPT_NOBODY=> false, //输出 BODY 部分
            CURLOPT_RETURNTRANSFER=> true, // 获取的信息以文件流的形式返回
            CURLOPT_SSL_VERIFYPEER=> false, //跳过ssl检查项
            CURLOPT_FOLLOWLOCATION=> false, //true 将会根据服务器返回 HTTP 头中的 "Location: " 重定向。（自动重定向无法继承POST参数和请求头，需要手动重定向）
            CURLOPT_AUTOREFERER=> false, //true 根据 Location: 重定向时，自动设置 header 中的Referer:信息。
            CURLOPT_TIMEOUT=> 120, //允许 cURL 函数执行的最长秒数。
        ];
        if ($post_data){
            //如果是POST的是数组类型数据，表示使用multipart/form-data数据编码
            //部分CURL版本不能自动计算Content-Length，所以此处需要自己进行数据编码后计算字符长度
            if (is_array($post_data)){
                $boundary = time();
                $post_data = self::multipart_form_data($post_data, $boundary);
                $len = strlen($post_data);
                $header_arr[] = "Content-Type: multipart/form-data; boundary={$boundary}";
                $header_arr[] = "Content-Length: {$len}";
            }
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); //设定 POST 数据，同时把Content-Type设定为 multipart/form-data 或者 application/x-www-form-urlencoded
        }
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header_arr);
        foreach ($option as $k=>$v){
            curl_setopt($ch, $k, $v);
        }
        foreach ($default_option as $k=>$v){
            if (!isset($option[$k])) curl_setopt($ch, $k, $v);
        }
        $res = curl_exec($ch);//执行请求
        $err = curl_error($ch);
        $curl_info = curl_getinfo($ch);
        curl_close($ch);//curl关闭
        if ($err) throw new \Exception('curl:'.$err);
        $header_size = $curl_info['header_size'] ?? null;
        if ($header_size === null){
            $arr = explode("\r\n\r\n", $res, ($curl_info['redirect_count']??0)+2);
            $body = array_pop($arr);
            $res_header = array_pop($arr);
        }else{
            $res_header = substr($res, 0, $header_size);
            $body = substr($res, $header_size);
        }
        //解析响应头
        $arr = explode("\r\n", $res_header);
        $res_header = [];
        foreach ($arr as $v){
            if (!strpos($v,': ')) continue;
            list($k,$v) = explode(': ', $v);
            $res_header[$k] = $v;
            $res_header[strtolower($k)] = $v;
        }
        //手动重定向
        if ($curl_info['http_code']>=300 && $curl_info['http_code']<400 && !empty($res_header['location'])){
            return self::curl($res_header['location'], $header_arr, $post_data, $option);
        }
        return ['code'=>$curl_info['http_code'],'header'=> $res_header, 'body'=> $body];
    }

    /**缓存
     * @param array|string $name 获取缓存传入缓存名(string)，设置缓存传入[缓存名=>缓存值]
     * @param mixed $default 获取缓存表示默认值(缓存过期或不存在)，设置缓存表示过期时间(秒数)
     * @return mixed|void
     * @throws Exception
     */
    protected function cache($name, $default=null)
    {
        self::assert(is_array($name) || is_string($name),"参数异常");
        if (is_string($name)) {
            $path = $this->config('cache_dir').$name.'.json';
            if (!is_file($path)) return $default;
            $data = json_decode(file_get_contents($path), true);
            if ($data['timeout']>0 && $data['timeout'] < time()) return $default;
            return $data['data'];
        }
        $timeout = $default ? time()+$default : -1;
        foreach ($name as $k=>$v){
            $path = $this->config('cache_dir').$k.'.json';
            $data = ['name'=>$k,'timeout'=>$timeout, 'data'=>$v];
            file_put_contents($path, json_encode($data,JSON_UNESCAPED_UNICODE));
        }
    }

    /**sdk接口请求通用方法
     * @param string $api 接口路径
     * @param array $param git请求参数，会附加到URL上
     * @param array $post_data POST发送参数
     * @param mixed $throw_token_exception 是否抛出 access_token 错误的异常，如果为false，当请求返回 errcode为40001时，会刷新access_token再重新发起请求
     * @return mixed
     * @throws SdkException
     */
    public function apiSdk($api, $param, $post_data=[], $throw_token_exception=false)
    {
        $url = 'https://api.weixin.qq.com/'.$api;
        if ($param) $url .= '?'.http_build_query($param);
        $res = self::curl($url,[],$post_data);
        self::assert($res['code']==200, '接口请求返回异常，http_code:'.$res['code']);
        $this->log(['url'=>$url,'config'=>$this->config,'request'=>['header'=>[],'body'=>$post_data],'response'=>$res]);
        //返回响应
        $data = json_decode($res['body'], true);

        if ($data===null) return $res['body'];
        if (isset($data['errcode'])){
            if (in_array($data['errcode'], [40001,40014,42001]) && isset($param['access_token']) && !$throw_token_exception){
                //access_token过期或者错误的时候，刷新access_token再尝试一次
                $param['access_token'] = $this->accessToken(true);
                return $this->apiSdk($api, $param, $post_data, true);
            }else if($data['errcode']!=0){
                throw new SdkException($data);
            }
        }
        return $data;
    }

    public function accessToken($refresh=false){
        $cache_key = 'accessToken_'.$this->config('appid');
        $access_token = $this->cache($cache_key);
        if (!$access_token || $refresh){//过期或者强制刷新
            $access_token_api = $this->config('access_token_api','');
            if ($access_token_api){
                try {
                    $post_data = [];
                    if ($refresh) $post_data['refresh'] = 1;
                    $res = self::curl($this->config('access_token_api',''), ['Accept: application/json'], $post_data);
                    $data = json_decode($res['body'], true);
                    $data = $data ?: [];
                    if (empty($data['access_token'])) throw new Exception($data['message']??'接口返回数据格式错误');
                    $access_token = $data['access_token'];
                } catch (Exception $e) {
                    self::assert(0,'从【access_token_api】获取access_token失败：'.$e->getMessage());
                }
            }else{
                $param = [
                    'grant_type'=>'client_credential',
                    'appid'=>$this->config('appid'),
                    'secret'=>$this->config('appsecret'),
                ];
                $res = $this->apiSdk('cgi-bin/token',$param);
                $access_token = $res['access_token'];
            }
            $this->cache([$cache_key=>$access_token], 7000);
        }
        return $access_token;
    }

}
